/** * FILE NAME: kms-bc-script.js * PURPOSE: Single Page Application state driver with optimistic UI rendering. * VERSION: 1.2.8 * GENERATED DATE/TIME: 2026-05-20 16:16:10 (Asia/Kuala_Lumpur) */ document.addEventListener('DOMContentLoaded', function () { const appContainer = document.getElementById('kmsbc-app'); if (!appContainer) return; const state = { currentToken: null, sessionDate: '', masterRoster: [], players: [], games: [], selectedCourtPlayers: [], shuttleCount: 1, pollingInterval: null, isSubmitting: false, activeEditingGameId: null }; const clubLogoUrl = 'https://i.postimg.cc/Bb0fZn67/Screenshot-2026-05-20-145748.jpg'; const getApiBaseUrl = () => { const currentPath = window.location.pathname; const appSlugIndex = currentPath.indexOf('/kms-court'); const subFolder = appSlugIndex > 0 ? currentPath.substring(0, appSlugIndex) : ''; return window.location.origin + subFolder + '/wp-json/kms-bc/v1/'; }; function init() { const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('session_id'); const emergencyLoaderKill = setTimeout(() => { showLoader(false); const wrapper = document.getElementById('kmsbc-screen-wrapper'); if (wrapper && wrapper.innerHTML === '') { if (token) { state.currentToken = token; renderScreenCourt(); } else { renderScreenSetup(); } } }, 1200); showLoader(true); fetchMasterRoster().then(() => { if (token) { state.currentToken = token; return fetchSessionStatus().then(() => { renderScreenCourt(); startPolling(); }); } else { renderScreenSetup(); } }).catch(err => { console.error('Initialization error:', err); window.history.replaceState({}, document.title, window.location.pathname); state.currentToken = null; renderScreenSetup(); }).finally(() => { clearTimeout(emergencyLoaderKill); showLoader(false); }); } function showLoader(visible) { const loader = document.getElementById('kmsbc-global-loader'); if (loader) { loader.style.display = visible ? 'flex' : 'none'; } } function apiRequest(endpoint, method = 'GET', data = null) { const targetUrl = getApiBaseUrl() + endpoint; const options = { method: method, headers: {} }; if (typeof kmsbc_config !== 'undefined' && kmsbc_config.nonce) { options.headers['X-WP-Nonce'] = kmsbc_config.nonce; } if (data) { if (method === 'POST') { options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; const formBody = []; for (const property in data) { const encodedKey = encodeURIComponent(property); const encodedValue = encodeURIComponent(data[property]); formBody.push(encodedKey + "=" + encodedValue); } options.body = formBody.join("&"); } else { options.headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(data); } } return fetch(targetUrl, options).then(response => { if (!response.ok) return response.json().then(err => { throw err; }); return response.json(); }); } function fetchMasterRoster() { return apiRequest('roster/get', 'GET').then(data => { state.masterRoster = Array.isArray(data) ? data : []; }).catch(err => { console.error('Master roster fetch error:', err); state.masterRoster = []; }); } function fetchSessionStatus() { return apiRequest('session/status?token=' + encodeURIComponent(state.currentToken), 'GET') .then(data => { state.sessionDate = data.session_date || ''; state.players = data.players || []; state.games = data.games || []; state.selectedCourtPlayers = state.selectedCourtPlayers.filter(p => state.players.includes(p)); }); } function startPolling() { if (state.pollingInterval) clearInterval(state.pollingInterval); state.pollingInterval = setInterval(() => { if (!state.isSubmitting && state.activeEditingGameId === null) { fetchSessionStatus().then(() => { updateLiveDashboardDOM(); }).catch(err => console.error('Polling error:', err)); } }, 4000); } function stopPolling() { if (state.pollingInterval) { clearInterval(state.pollingInterval); state.pollingInterval = null; } } function renderShuttlecockSVG() { return ` `; } function renderScreenSetup() { stopPolling(); const wrapper = document.getElementById('kmsbc-screen-wrapper'); if (!wrapper) return; let html = `

KMS BC

Select tonight's active group roster. Minimum 4 players required to unlock fields.

0 / 4 Players Checked In

Manage Group Database Pool

`; wrapper.innerHTML = html; updateSetupScreenDOM(); document.getElementById('btn-add-roster-submit').addEventListener('click', executeMasterRosterAdd); document.getElementById('txt-new-roster-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') executeMasterRosterAdd(); }); document.getElementById('btn-start-session').addEventListener('click', function () { const checked = wrapper.querySelectorAll('.kmsbc-setup-chk:checked'); const selectedPlayers = Array.from(checked).map(c => c.value); showLoader(true); apiRequest('session/init', 'POST', { players: JSON.stringify(selectedPlayers) }) .then(res => { state.currentToken = res.token; const newUrl = window.location.pathname + '?session_id=' + encodeURIComponent(res.token); window.history.pushState({ path: newUrl }, '', newUrl); return fetchSessionStatus(); }) .then(() => { renderScreenCourt(); startPolling(); }) .catch(err => alert(err.error || 'Exception generated starting session.')) .finally(() => showLoader(false)); }); } function updateSetupScreenDOM() { const checkboxContainer = document.getElementById('setup-checkboxes-container'); const rosterContainer = document.getElementById('master-roster-edit-container'); if (!checkboxContainer) return; let checkHtml = ''; state.masterRoster.forEach((player, index) => { checkHtml += ` `; }); checkboxContainer.innerHTML = checkHtml || '

Database pool empty.

'; const checkboxes = checkboxContainer.querySelectorAll('.kmsbc-setup-chk'); const startBtn = document.getElementById('btn-start-session'); const badge = document.getElementById('setup-count-badge'); checkboxes.forEach(chk => { chk.addEventListener('change', () => { const checked = checkboxContainer.querySelectorAll('.kmsbc-setup-chk:checked'); const count = checked.length; const parentTile = chk.closest('.kmsbc-interactive-checkbox-tile'); if (chk.checked) parentTile.classList.add('tile-is-checked'); else parentTile.classList.remove('tile-is-checked'); badge.innerText = `${count} / 4 Players Checked In`; if (count >= 4) { startBtn.removeAttribute('disabled'); badge.classList.add('badge-is-valid'); } else { startBtn.setAttribute('disabled', 'disabled'); badge.classList.remove('badge-is-valid'); } }); }); let rosterHtml = ''; state.masterRoster.forEach(player => { rosterHtml += `
`; }); rosterContainer.innerHTML = rosterHtml; rosterContainer.querySelectorAll('.kmsbc-btn-save-roster').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.getAttribute('data-id')); const input = rosterContainer.querySelector(`input[data-id="${id}"]`); executeMasterRosterEdit(id, input.value.trim(), 'update'); }); }); rosterContainer.querySelectorAll('.kmsbc-btn-delete-roster').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.getAttribute('data-id')); if (confirm('Permanently wipe this player profile entirely out of the group directory database?')) { executeMasterRosterEdit(id, '', 'delete'); } }); }); } function executeMasterRosterAdd() { const input = document.getElementById('txt-new-roster-name'); const name = input.value.trim(); if (!name) return; showLoader(true); apiRequest('roster/add', 'POST', { player_name: name }) .then(() => { input.value = ''; return fetchMasterRoster(); }) .then(() => updateSetupScreenDOM()) .catch(err => alert(err.error || 'Failed adding profile.')) .finally(() => showLoader(false)); } // FIXED: Uses absolute Optimistic UI overrides to instantly clean components from local state matrices function executeMasterRosterEdit(id, name, action) { if (action === 'delete') { // Optimistic UX: Strip row and record from active screen cache *instantly* state.masterRoster = state.masterRoster.filter(p => p.id !== id); updateSetupScreenDOM(); } showLoader(true); apiRequest('roster/edit', 'POST', { id: id, player_name: name, action: action }) .then(() => { // Background refresh to confirm server persistence matching data structures return fetchMasterRoster(); }) .then(() => updateSetupScreenDOM()) .catch(err => { console.error('Server sync reject:', err); return fetchMasterRoster().then(() => updateSetupScreenDOM()); }) .finally(() => showLoader(false)); } function renderScreenCourt() { const wrapper = document.getElementById('kmsbc-screen-wrapper'); if (!wrapper) return; wrapper.innerHTML = `
Session: ${state.sessionDate}

Stadium Arena Court

Shuttlecocks Consumed:
1

The Active Court Bench (Tap 4 onto court)

Chronological Match History

`; document.getElementById('btn-back-setup').addEventListener('click', () => { if (confirm('Exit session view? Active rows remain safely synced.')) { window.history.replaceState({}, document.title, window.location.pathname); state.currentToken = null; renderScreenSetup(); } }); document.getElementById('btn-shuttle-minus').addEventListener('click', () => { if (state.shuttleCount > 1) { state.shuttleCount--; updateControlsDOM(); } }); document.getElementById('btn-shuttle-plus').addEventListener('click', () => { state.shuttleCount++; updateControlsDOM(); }); document.getElementById('btn-log-game').addEventListener('click', executionLogGameMatch); document.getElementById('btn-end-session').addEventListener('click', renderScreenSummary); document.getElementById('btn-cancel-edit-mode').addEventListener('click', () => { state.activeEditingGameId = null; state.selectedCourtPlayers = []; state.shuttleCount = 1; updateLiveDashboardDOM(); }); updateLiveDashboardDOM(); } function updateLiveDashboardDOM() { if (!document.getElementById('court-slots-container')) return; const cancelEditBtn = document.getElementById('btn-cancel-edit-mode'); const courtHeading = document.getElementById('court-panel-heading-title'); if (state.activeEditingGameId !== null) { courtHeading.innerHTML = `✏️ Editing Match Log #${state.activeEditingGameId}`; courtHeading.classList.add('kmsbc-text-warning'); cancelEditBtn.style.display = 'block'; } else { courtHeading.innerHTML = 'Stadium Arena Court'; courtHeading.classList.remove('kmsbc-text-warning'); cancelEditBtn.style.display = 'none'; } const courtContainer = document.getElementById('court-slots-container'); let courtHtml = ''; for (let i = 0; i < 4; i++) { const player = state.selectedCourtPlayers[i]; if (player) { courtHtml += `
${player} ×
`; } else { courtHtml += `
Available Slot
`; } } courtContainer.innerHTML = courtHtml; courtContainer.querySelectorAll('.position-slot-occupied').forEach(slot => { slot.addEventListener('click', () => { const idx = parseInt(slot.getAttribute('data-index')); state.selectedCourtPlayers.splice(idx, 1); updateLiveDashboardDOM(); }); }); const benchContainer = document.getElementById('bench-slots-container'); benchContainer.innerHTML = ''; state.players.forEach(player => { const isSelected = state.selectedCourtPlayers.includes(player); const btn = document.createElement('button'); btn.type = 'button'; btn.className = `kmsbc-bench-interactive-pill-node ${isSelected ? 'pill-node-is-active' : ''}`; btn.innerText = player; btn.addEventListener('click', () => { if (isSelected) { state.selectedCourtPlayers = state.selectedCourtPlayers.filter(p => p !== player); } else { if (state.selectedCourtPlayers.length < 4) { state.selectedCourtPlayers.push(player); } else { alert('Court full. Clear a slot to replace positions.'); return; } } updateLiveDashboardDOM(); }); benchContainer.appendChild(btn); }); const addCard = document.createElement('div'); addCard.className = 'kmsbc-bench-latecomer-input-inline-card'; addCard.innerHTML = ` `; benchContainer.appendChild(addCard); const inlineInput = document.getElementById('txt-late-name'); const inlineBtn = document.getElementById('btn-late-submit'); const executeLatecomerAdd = () => { const value = inlineInput.value.trim(); if (!value) return; if (state.players.some(p => p.toLowerCase() === value.toLowerCase())) { alert('This user signature already checked in on tonight\'s bench.'); return; } state.isSubmitting = true; showLoader(true); apiRequest('player/add', 'POST', { token: state.currentToken, player_name: value }) .then(() => { inlineInput.value = ''; return fetchSessionStatus(); }) .then(() => updateLiveDashboardDOM()) .catch(err => alert(err.error || 'Failed adding latecomer.')) .finally(() => { state.isSubmitting = false; showLoader(false); }); }; inlineBtn.addEventListener('click', executeLatecomerAdd); inlineInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') executeLatecomerAdd(); }); const historyContainer = document.getElementById('history-slots-container'); if (state.games.length === 0) { historyContainer.innerHTML = '

No matches registered yet tonight.

'; } else { let historyHtml = ''; historyContainer.innerHTML = historyHtml; historyContainer.querySelectorAll('.kmsbc-btn-action-edit-historical-match').forEach(editBtn => { editBtn.addEventListener('click', () => { const gameId = parseInt(editBtn.getAttribute('data-id')); const targetGame = state.games.find(g => g.id === gameId); if (targetGame) { state.activeEditingGameId = gameId; state.selectedCourtPlayers = [...targetGame.players]; state.shuttleCount = targetGame.shuttles; updateLiveDashboardDOM(); document.getElementById('kmsbc-app').scrollTop = 0; } }); }); historyContainer.querySelectorAll('.kmsbc-btn-action-delete-historical-match').forEach(delBtn => { delBtn.addEventListener('click', () => { const gameId = delBtn.getAttribute('data-id'); if (confirm('Permanently purge this match entry record?')) { state.isSubmitting = true; showLoader(true); apiRequest('game/delete', 'POST', { token: state.currentToken, game_id: gameId }) .then(() => { if (state.activeEditingGameId == gameId) state.activeEditingGameId = null; return fetchSessionStatus(); }) .then(() => updateLiveDashboardDOM()) .catch(err => alert(err.error || 'Purge exception processing historical drops.')) .finally(() => { state.isSubmitting = false; showLoader(false); }); } }); }); } updateControlsDOM(); } function updateControlsDOM() { const logBtn = document.getElementById('btn-log-game'); const shuttleLbl = document.getElementById('lbl-shuttle-count'); if (!logBtn) return; shuttleLbl.innerText = state.shuttleCount; const baseCostTotal = state.shuttleCount * 12; if (state.activeEditingGameId !== null) { logBtn.innerText = `Update Match Entry (RM ${baseCostTotal})`; logBtn.className = 'kmsbc-btn-action kmsbc-btn-action-warning-update'; } else { logBtn.innerText = `Log Active Match (RM ${baseCostTotal} Total)`; logBtn.className = 'kmsbc-btn-action kmsbc-btn-action-success'; } if (state.selectedCourtPlayers.length === 4) { logBtn.removeAttribute('disabled'); } else { logBtn.setAttribute('disabled', 'disabled'); } } function executionLogGameMatch() { if (state.selectedCourtPlayers.length !== 4) return; state.isSubmitting = true; showLoader(true); const dataPayload = { token: state.currentToken, players: JSON.stringify(state.selectedCourtPlayers), shuttles: state.shuttleCount }; if (state.activeEditingGameId !== null) { dataPayload.game_id = state.activeEditingGameId; } apiRequest('game/log', 'POST', dataPayload) .then(() => { state.selectedCourtPlayers = []; state.shuttleCount = 1; state.activeEditingGameId = null; return fetchSessionStatus(); }) .then(() => updateLiveDashboardDOM()) .catch(err => alert(err.error || 'Server error logging match layout.')) .finally(() => { state.isSubmitting = false; showLoader(false); }); } function renderScreenSummary() { if (!confirm('Close session tracking and output individual dynamic split-billing totals?')) { return; } stopPolling(); showLoader(true); fetchSessionStatus().then(() => { const wrapper = document.getElementById('kmsbc-screen-wrapper'); const userTabs = {}; let globalTotalShuttles = 0; state.players.forEach(p => { userTabs[p] = { shuttlesFractional: 0, cashOwedRM: 0 }; }); state.games.forEach(game => { globalTotalShuttles += game.shuttles; const matchCost = game.shuttles * 12; const playerCount = game.players.length; const costPerHead = matchCost / playerCount; const shuttlesPerHead = game.shuttles / playerCount; game.players.forEach(p => { if (userTabs[p]) { userTabs[p].shuttlesFractional += shuttlesPerHead; userTabs[p].cashOwedRM += costPerHead; } }); }); const reportingList = Object.keys(userTabs).map(name => { return { name: name, shuttles: userTabs[name].shuttlesFractional, cost: userTabs[name].cashOwedRM }; }); reportingList.sort((a, b) => b.cost - a.cost); let html = `
📋

Session Statement

Court Date: ${state.sessionDate}
Total Shuttlecocks Split: ${globalTotalShuttles} Units
`; reportingList.forEach((row, i) => { const displayShuttles = Number(row.shuttles.toFixed(2)).toString(); const displayCost = row.cost.toFixed(2); html += ` `; }); html += `
# Player Shuttles Fee Owed
${i + 1} ${row.name} ${displayShuttles} RM ${displayCost}
`; wrapper.innerHTML = html; document.getElementById('btn-restart-app').addEventListener('click', () => { window.history.replaceState({}, document.title, window.location.pathname); state.currentToken = null; state.selectedCourtPlayers = []; state.shuttleCount = 1; renderScreenSetup(); }); document.getElementById('btn-share-whatsapp').addEventListener('click', () => { let textBlock = `🏸 *Badminton Session Summary*\n`; textBlock += `📅 Date: ${state.sessionDate}\n`; textBlock += `Total Shuttles Used: ${globalTotalShuttles}\n\n`; textBlock += `*Fee Breakdown:*\n`; reportingList.forEach((row, i) => { const displayShuttles = Number(row.shuttles.toFixed(2)).toString(); const displayCost = row.cost.toFixed(2); textBlock += `${i + 1}. ${row.name} (${displayShuttles} shuttles) = RM ${displayCost}\n`; }); textBlock += `\nPlease QR the payment to our admin (Me). Thanks for the games tonight! 🙏`; const whatsappUrl = `https://api.whatsapp.com/send?text=` + encodeURIComponent(textBlock); window.open(whatsappUrl, '_blank'); }); }).catch(err => { alert(err.error || 'Failed generating spreadsheet calculation sheets.'); }).finally(() => { showLoader(false); }); } init(); });